import os
import sys
import inspect
from uuid import uuid4
#from xmlrpclib import dumps, loads, Fault
from json import dumps, loads
from twisted.internet import reactor, task, defer
from twisted.python.failure import Failure
from twisted.internet.protocol import ClientCreator
from txamqp.protocol import AMQClient
from txamqp.client import TwistedDelegate
from txamqp.queue import Closed
from txamqp.content import Content
from mediadart.mqueue import xmlspec
from mediadart.mqueue.mqserver import RPCError
from mediadart.utils import get_mq_params
from mediadart.config import Configurator
from mediadart import log
import txamqp


_theProxyFactory = None
_aLock = defer.DeferredLock()   # protects access to _theProxyFactory

VERBOSE = 0    # set to log content of every message processed
class _Proxy:
    # some magic to bind an XML-RPC method to an RPC server.
    # supports "nested" methods (e.g. examples.getStateName)
    def __init__(self, factory, server_name, reply_url):
        self.__factory = factory
        self.__server_name = server_name
        self.__reply_url = reply_url
        self.__method_name = ''
        #if self.__factory._failed:
        #    raise RPCError('start-up failure')
        d=factory._lock.acquire()
        d.addCallback(factory._setup_target_queue, server_name)
        d.addErrback(factory._setup_error, 'di questo')

    def __getattr__(self, name):
        if self.__method_name:
            self.__method_name += ('.' + name)
        else:
            self.__method_name = name
        return self

    def __call__(self, *args, **kwargs):
        #log.debug('__call__')
        method_name, self.__method_name = self.__method_name, ''
        d = self.__factory._lock.acquire()
        d.addCallback(self.__factory._invoke, self.__server_name, self.__reply_url,  method_name, args, kwargs)
        return d

    def __str__(self):
        return "Proxy(%s)" % self.__server_name


class ProxyFactory:
    """to [,options] -> a logical connection to an XML-RPC server

    uri the name of the class of the server to be invoked.

    The following options can be given at initialization as keyword arguments:
        host: the address of the  message broker.
        port: port of the message broker
        auth: a tuble ('username', 'password') to authenticate with the
        broker.

    Encoding of all strings is UTF-8. All strings passed to the server
    assume this encoding.
    """


    def __init__(self, host, port, username, password, vhost, lock):
        self._client = None
        self._failed = False
        self._lock = lock    # already acquired at creation
        self._tag = '%x' % id(self)
        self.__chid = None
        self._channel = None
        self._outstanding = {}  # list of ids of msg waiting for a response
        self._targets = []
        self.__allow_none = False
        self.__encoding = 'utf-8'
        d = self.__connect(host, port, username, password, vhost)
        d.addCallback(self._setup)
        d.addErrback(self._setup_error)


    def close(self, reason=''):
        if not self._client:
            return
        self._client.close(reason)
        for d in self._outstanding.values():
            d.errback(Failure(RPCError('Connection closed')))
        self._client = None

    def __connect(self, host, port, user, password, vhost):
        " Connect to broker, authenticate and create AMQClient "
        def cb(client):
            d=client.authenticate(user, password).addCallbacks(cb1, callbackArgs=[client])
            return d

        def cb1(result, client):
            if client:
                self._client = client
            #log.debug('connection ok')
            return 'connection ok'

        self._client = 'locked'
        spec_path = os.path.dirname(os.path.abspath(inspect.getfile(ProxyFactory)))
        specfile = os.path.join(spec_path, xmlspec)
        spec = txamqp.spec.load(specfile)
        delegate = TwistedDelegate()
        d = ClientCreator(reactor, AMQClient, TwistedDelegate(), vhost, spec).connectTCP(host, int(port))
        d.addCallback(cb)
        return d

    def _setup(self, result ):   
        "Open channel and return_queue (error msg) and reply queue (normal messages)"
        def cb1(channel_id):
            #log.debug('cb1: opening channel')
            self._channel = channel_id
            self._channel.channel_open().addCallbacks(cb2, self._setup_error)

        def cb2( result ):
            #log.debug('cb2: exchange declare')
            self._log_msg('cb2: exchange declare %s', result)
            self._channel.exchange_declare(exchange="amq.direct", type="direct").addCallbacks(cb3, self._setup_error)

        def cb3( result ):
            #log.debug('cb3: queue_declare')
            self._log_msg('cb3: queue_declare', result)
            self._channel.queue_declare(queue="", exclusive=True).addCallbacks(cb4, self._setup_error)

        def cb4( result ):
            #log.debug('cb4: queue_bind')
            self._log_msg('cb4: queue_bind', result)
            self._reply_queue = result[0]
            self._channel.queue_bind(queue=self._reply_queue, exchange="amq.direct", 
                  routing_key=self._reply_queue).addCallbacks(cb5, self._setup_error)

        def cb5( result ):
            #log.debug('cb5: basic_consume')
            self._log_msg('cb5: basic_consume', result)
            self._channel.basic_consume(queue=self._reply_queue, no_ack=True, 
                 consumer_tag=self._tag).addCallbacks(cb6, self._setup_error)

        def cb6( result ):
            #log.debug( 'cb6: getting queue')
            self._log_msg( 'cb6: getting queue', result )
            self._client.queue(self._tag).addCallbacks(cb7, self._setup_error)

        def cb7 ( queue ):
            "receives the queue with key self._tag and stores the handler in the queue"
            #log.debug( 'cb7: listening' )
            self._next_msg(queue, 'response')
            self._next_msg(self._client.basic_return_queue, 'return')  # register for return messages
            log.debug('__setup ok')
            self._lock.release()

        #log.debug('MQServerProyx.setup()')
        self.__chid = len(self._client.channels) + 100   # unique-leave low values for txamqp
        self._client.channel(self.__chid).addCallbacks(cb1, self._setup_error)

    def _setup_error(self, failure, msg=''):
        log.error('ProxyFactory(%s): connection error %s' % (msg, failure.getErrorMessage()))
        self._failed = True
        for d in self._outstanding.values():
            d.errback(failure)
        self._lock.release()
        return ''

    def _setup_target_queue(self, lock, name):    # <_> is the lock, unused here
        "Open queue for server <name>k"
        def cb1( _ ):
            log.debug('_setup_target_queue: queue_bind')
            self._channel.queue_bind(queue=name, exchange="amq.direct", 
                  routing_key=name).addCallbacks(cb2, self._setup_error)

        def cb2( _ ):
            self._targets.append(name)
            lock.release()

        ret = 'ok'
        if self._failed or not name in self._targets:
            if self._failed:
                log.debug('_setup_target_queue: reconnecting to queue %s after failure' % name)
            else:
                log.debug('_setup_target_queue: connecting to queue %s (%s)' % (name, id(self)))
            self._failed = False
            ret = self._channel.queue_declare(queue=name, passive=True, auto_delete=True)
            ret.addCallback(cb1)
        else:
            log.debug('_setup_target_queue(%s): doing nothing' % name)
            lock.release()
        return ret

    def _next_msg(self, queue, qtype):
        """qtype is "response" for the queue to receive responses from servers and 
           "return" for the queue to receive return messages from the broker.
        """
        d = queue.get()
        d.addCallback(self._handle_msg, queue, qtype)
        d.addErrback(self._handle_err, queue, qtype)
        
    def _handle_err(self, failure, queue, qtype):
        #log.debug('_handle_err %s' % qtype)
        if failure.check(Closed):
            log.debug('channel closed')
        else:
            log.debug('error: %s' % failure)
            self._next_msg(queue, qtype)

    def _handle_msg(self, msg, queue, qtype):
        self._log_msg('_handle_msg:', msg)
        self._next_msg(queue, qtype)
        if hasattr(msg, 'content') and hasattr(msg.content, 'properties'):
            msg_id = msg.content.properties.get('correlation id', 'correlation id missing in message')
            try:
                d = self._outstanding.pop(msg_id)
                if qtype == 'return':
                    log.debug('received a return message')
                    d.errback(Failure(RPCError('undeliverable request (server down or unreacheable)')))

                elif qtype == 'response':
                    resp = loads(msg.content.body)
                    if resp.get('error', None) is not None:
                        d.errback(Failure(resp['error']))
                    else:
                        d.callback(resp['result'])
                else:
                    log.error('Internal error')
            except KeyError, e:
                log.error('unmatched message: %s' % msg_id)
                log.error('MORE INFO: %s %s' % (type(e), e))
            except ValueError, e:
                log.error('Error decoding message: %s' % str(e))
        else:
            log.error('Received unrecognized message')

    def _invoke(self, lock, servername, request_url, methodname, params, kwparams):
        lock.release()
        log.debug('_invoke %s.%s' % ( servername, methodname))
        d = defer.Deferred()
        if self._failed:
            d.errback(Failure(RPCError('proxy failed to connect')))
            return d
        req_id = uuid4().get_hex()
        if not kwparams:
            p = params
        else:
            n = 0
            for p in params:
                kwparams[str(n)] = p
                n += 1
            p = kwparams
        #log.debug('request params = %s'  % str(p))
        request = {'method': methodname, 'params': p, 'id':req_id, 'version':'1.1',}
        log.debug('_invoke: executing %s, id = %s' % (methodname, req_id))
        log.debug('invoke: %s' % request)
        request_str = dumps(request)
        msg = Content(request_str)
        msg['delivery mode'] = 1
        msg['correlation id'] = req_id
        if request_url:
            msg['reply to'] = request_url
            d.callback('request sent')
        else:
            msg['reply to'] = self._reply_queue
            self._outstanding[req_id] = d
            #log.debug('out: Outstanding: %s' % '\n'.join(self._outstanding.keys()))
        self._channel.basic_publish(exchange='amq.direct', immediate=1, mandatory=1, content=msg, routing_key=servername)
        return d

        
    def __repr__(self):
        return "<ProxyFactory>"

    def __str__(self):
        return self.__repr__()

    if VERBOSE:
        def _log_msg(self, tag, msg):
            "used for tests"
            log.debug('%s %s' % ('>>>>>>>>>', tag))
            log.debug('Message %s' % msg.method)
            if hasattr(msg, 'content'):
                log.debug(' content: %s' % msg.content)
                if hasattr(msg.content, 'properties'):
                    log.debug(' content.properties.: %s' % msg.content.properties)
            if hasattr(msg, 'fields'):
                log.debug(' fields: %s' % str(msg.fields))
            log.debug('<'*30 + '\n')
    else:
        def _log_msg(self, tag, msg): pass


# external interface

def _create_factory(broker_ip, broker_port, username, password, vhost):
    global _aLock

    def cb(lock):
        global _theProxyFactory
        if _theProxyFactory is None or _theProxyFactory._failed:
            log.debug("Creating new proxy")
            c = Configurator()
            c.reload()
            _theProxyFactory = ProxyFactory(broker_ip, broker_port, username, password, vhost, lock)
        else:
            lock.release()

    d = _aLock.acquire()
    d.addCallback(cb)

def Proxy(server_name, reply_url='', host=None, port=None, username=None, password=None, vhost=None):
    global _theProxyFactory
    _create_factory(**get_mq_params(host, port, username, password, vhost))
    return _Proxy(_theProxyFactory, server_name, reply_url)

def Shutdown():
    if _theProxyFactory is None:
        return
    _theProxyFactory.close()
    _theProxyFactory = None





